In this example we start with a simple domain model and incrementally migrate it to become more realistic. Along the way we end up using several features of JPA not yet covered by the previous tutorials.
The Problem
[Invalid Include: Page not found: JA Tutorial 3 - The Problem]
V1 Project Setup
For the rest of this section, when you see <project>, replace it with JpaTutorial3
Create Java Project
Next we need to create a Java project. We'll keep the source separate from the bin directory:
Pull down the File menu and select New:Project
Select Java Project and click on Next
Enter a project name: <project>, again read this to know why I did not use a space in the project name.
Make sure "Create new project in workspace" is selected.
Make sure the JRE selected is 1.5.x or higher. If such a JRE does not show in the list, you can add it through Window->Preferences->JAVA->Installed JRE's.
Select Create separate source and output folders
Click Finish
Create folders and packages
Expand your <project> folder
Select the src directory
Right-click, select new:Folder
Use the name META-INF
Make sure <project> is still selected, right-click and select New:Source Folder
Enter test and click OK
Add Required Libraries
We now need to add two libraries. Note that these steps assume you've already worked through the first tutorial and are working in the same workspace. If you, you'll need to create user libraries. Review Creating User Libraries.
Edit the project properties. Select your <project> and either press alt-enter or right-click and select properties.
Select Java Build Path
Click on the Libraries tab
Click on Add Library
Select User Libraries and click Next
Select JPA_JSE by click on the check box
Click OK
Click on Add Library again
Click on JUnit
Click Next
In the pull-down list, select JUnit 4
Click Finish
Click OK
If you'd like some background information on JUnit, please go here.
Configure Persistence Unit
We now need to create the Persistent Unit definition. We are going to create a file called persistence.xml in the src/META-INF directory with the following contents:
Find the src/META-INF directory (if one does not exist, right-click, select New:Folder, enter META-INF and press enter)
Right click the src/META-INF, select new:File.
Enter persistence.xml for the name and press "OK" (Note: all lowercase. It won't make a difference on Windows XP but it will on Unix.)
Copy the contents (above) into the file and save it
Update persistence.xml
Update Persistence Unit Name
The name of the persistence unit in your just-copied persistence.xml is examplePersistenceUnit in this example we use lis for Library Information System. Make the following change:
A Little Context
Before we get started, this tutorial is deliberately organized in an inconvenient fashion. Why? My target reader is someone in a class I'm teaching (the material is open-source but still targeted). I do not want the student to be able to quickly just copy the whole thing and get it to work without having to put forth some effort. In a classroom situation, I have all of the source code available if I need to help a student get up to speed.
We'll start with a stripped down version of the requirements. This first test suite handles the following test cases:
Create a Patron
Remove a Patron
Update a Patron
Attempt to find Patron that does not exist.
Notice that this suite of tests is for Creating, Reading, Updating and Deleting (CRUD) Patrons.
Assuming you've done Tutorial 2, much of the boilerplate code is going to look the same. First let's write a unit test for each of these test cases: Create a Patron
@Test
publicvoid createAPatron(){final Patron p = createAPatronImpl();final Patron found = dao.retrieve(p.getId());
assertNotNull(found);}private Patron createAPatronImpl(){final Address a = new Address("5080 Spectrum Drive", "Suite 700 West",
"Addison", "TX", "75001");return dao.createPatron("Brett", "Schuchert", "972-555-1212", a);}
This test first creates a patron using a private utility method. This method exists because it is used later in other unit tests.
Looking at the test, it uses an attribute called dao. This is a Data Access Object (which we'll later convert to a stateless Session Bean). This Data Access Object will be responsible for retrieving, creating and removing Patrons. Remove a Patron
@Test
publicvoid removeAPatron(){final Patron p = createAPatronImpl();
dao.removePatron(p.getId());final Patron found = dao.retrieve(p.getId());
assertNull(found);}
This test uses the utility method to create a patron. It then removes it and makes sure that when we try to retrieve it that the Patron no longer exists.
Update a Patron
@Test
publicvoid updateAPatron(){final Patron p = createAPatronImpl();finalString originalPhoneNumber = p.getPhoneNumber();
p.setPhoneNumber(NEW_PN);
dao.update(p);final Patron found = dao.retrieve(p.getId());
assertNotNull(found);
assertFalse(NEW_PN.equals(originalPhoneNumber));
assertEquals(NEW_PN, p.getPhoneNumber());}
We create a patron then update it.
Attempt to find Patron that does not exist
@Test
publicvoid tryToFindPatronThatDoesNotExist(){finalLong id = -18128129831298l;final Patron p = dao.retrieve(id);
assertNull(p);}
Verify that when we try to find a patron that's not found, we get back null.
Supporting Code
We have several veterans returning from previous tutorials. And here they are:
PatronDaoTest
First the imports and the attributes. Note that this is a complete class that will compile. It just doesn't do anything yet.
Initialization of the Logger
This is our 1-time initialization of the logging system.
@BeforeClass
publicstaticvoid initLogger(){// Produce minimal output.
BasicConfigurator.configure();// Comment this line to see a lot of initialization// status logging.Logger.getLogger("org").setLevel(Level.ERROR);}
Getting EMF and EM
Now before each unit test we'll look up the entity manager factory, create a dao, create an entity manager and pass it into a DAO and finally start a transaction.
@Before
publicvoid initEmfAndEm(){
emf = Persistence.createEntityManagerFactory("lis");
dao = new PatronDao();
dao.setEm(emf.createEntityManager());
dao.getEm().getTransaction().begin();}
Clean up after each test
After each test we'll rollback the transaction we created in the pre-test initialization. We'll then close both the entity manager and entity manager factory. This keeps our tests isolated.
We need to create entities. These entities are a bit more well-specified that what you've seen in the previous tutorials. In most cases I believe the extra information is intuitive. Where it is not, I'll try to point out what is going on.
Address.java
packageentity;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;
@Entity// The next class is a JPA entitypublicclass Address {
@Id // The next attribute (in this case) or method is a key field
@GeneratedValue // the key is auto-generatedprivateLong id;
@Column(length = 50)// the next column is 50 characters in sizeprivateString streetAddress1;
@Column(length = 50)privateString streetAddress2;
@Column(length = 20)privateString city;
@Column(length = 2)privateString state;
@Column(length = 9)privateString zip;public Address(){}public Address(finalString sa1, finalString sa2, finalString city,
finalString state, finalString zip){
setStreetAddress1(sa1);
setStreetAddress2(sa2);
setCity(city);
setState(state);
setZip(zip);}publicString getCity(){return city;}publicvoid setCity(finalString city){this.city = city;}publicString getState(){return state;}publicvoid setState(finalString state){this.state = state;}publicString getStreetAddress1(){return streetAddress1;}publicvoid setStreetAddress1(finalString streetAddress1){this.streetAddress1 = streetAddress1;}publicString getStreetAddress2(){return streetAddress2;}publicvoid setStreetAddress2(finalString streetAddress2){this.streetAddress2 = streetAddress2;}publicString getZip(){return zip;}publicvoid setZip(finalString zip){this.zip = zip;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
Patron.java
packageentity;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;
@Entity// the next class is a JPA entitypublicclass Patron {
@Id
@GeneratedValue
privateLong id;// when in the database, this field cannot be null, as an object in memory// it can
@Column(length = 20, nullable = false)privateString firstName;
@Column(length = 30, nullable = false)privateString lastName;
@Column(length = 11, nullable = false)privateString phoneNumber;// This next field refers to an object that is stored in another table.// All updates are cascaded. So if you persist me, my address, which is in// another table, will be persisted automatically. Updates and removes are// also cascaded automatically.
@OneToOne(cascade = CascadeType.ALL)private Address address;public Patron(finalString fName, finalString lName, finalString phone,
final Address a){
setFirstName(fName);
setLastName(lName);
setPhoneNumber(phone);
setAddress(a);}public Address getAddress(){return address;}publicvoid setAddress(Address address){this.address = address;}publicString getFirstName(){return firstName;}publicvoid setFirstName(String firstName){this.firstName = firstName;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}publicString getLastName(){return lastName;}publicvoid setLastName(String lastName){this.lastName = lastName;}publicString getPhoneNumber(){return phoneNumber;}publicvoid setPhoneNumber(String phoneNumber){this.phoneNumber = phoneNumber;}}
Finally, the Data Access Object
The DAO has the following four methods:
createPatron
retrieve
removePatron
update
We'll look at each of these, although none of this will be new if you've looked at the first tutorial. createPatron
Given the information to create a patron, instantiate one and then persiste it. Note that this is written in a style that will natually fit into a Session Bean.
public Patron createPatron(finalString fname, finalString lname,
finalString phoneNumber, final Address a){final Patron p = new Patron(fname, lname, phoneNumber, a);
getEm().persist(p);return p;}
retrieve
This uses the find method built into the entity manager. It returns null if not found. It first sees if the object is in the first-level cache. If not, it retrieves it from the database.
public Patron retrieve(finalLong id){return getEm().find(Patron.class, id);}
removePatron
To remove an object we have to find it first. You do not provide a class and a key. So we first retrieve it (it might already be in the cache so this may not involve a database hit. We then issue the remove of the object.
publicvoid removePatron(finalLong id){final Patron p = retrieve(id);if(p != null){
getEm().remove(p);}}
update
Update uses the merge method to get its work done. Note that it returns what is returned from merge. Why? The provided patron could be detached (retrieve during another transaction or from a different instance of an entity manager. If this is the case, then the object will not be put into the cache (and therefore become managed). Instead, a new instance is created and the contents of the paramter is copied into the new, managed instance. That new managed instance is returned. If the provided patron is managed, then there's actually not need to even call this method because any changes made to the patron will be reflected in the patron after the transaction is closed.
public Patron update(final Patron p){return getEm().merge(p);}
The rest of the class
Here are the imports, attributes and getters/setters.
We started with Patron. In round 2, we add the basic support for the Book. The book dao needs the same basic tests:
Creating a Book
Removing a Book
Updating a Bookadd
Note that we did not also include retrieving a book. We use this functionality in all of the tests anyway so I do not include a specific test for that functionality. This might seem like we’re not isolating tests perfectly but then I’ve never seen or come up with a “perfect” solution to this issue and this seems adequate to me.
We've already written a test very much like the above list if you consider PatronTest. We can extract quite a bit of common code out of our PatronTest and reuse it in our BookTest class. Take a look at this base class (note the embedded comments contain background information): BaseDbDaoTest.java
packagesession;importjavax.persistence.EntityManagerFactory;importjavax.persistence.Persistence;importorg.apache.log4j.BasicConfigurator;importorg.apache.log4j.Level;importorg.apache.log4j.Logger;importorg.junit.After;importorg.junit.Before;importorg.junit.BeforeClass;/**
* A base class for tests that handles logger initialization, entity manager
* factory and entity manager creation, associating an entity manager with a
* dao, starting and rolling back transactions.
*/publicabstractclass BaseDbDaoTest {private EntityManagerFactory emf;/**
* Once before the tests start running for a given class, init the logger
* with a basic configuration and set the default reporting layer to error
* for all classes whose package starts with org.
*/
@BeforeClass
publicstaticvoid initLogger(){// Produce minimal output.
BasicConfigurator.configure();// Comment this line to see a lot of initialization// status logging.Logger.getLogger("org").setLevel(Level.ERROR);}/**
* Derived class is responsible for instantiating the dao. This method gives
* the hook necessary to this base class to init the dao with an entity
* manger in a per-test setup method.
*
* @return The dao to be used for a given test. The type specified is a base
* class from which all dao's inherit. The test derived class will
* override this method and change the return type to the type of
* dao it uses. This is called **covariance**. Java 5 allows
* covariant return types. I.e. BookDaoTest's version of getDao()
* will return BookDao while PatronDao's version of getDao() will
* return Patron.
*/publicabstract BaseDao getDao();/**
* Before each test method, look up the entity manager factory, get the dao
* and set a newly-created entity manager and begin a transaction.
*/
@Before
publicvoid initEmfAndEm(){
emf = Persistence.createEntityManagerFactory("lis");
getDao().setEm(emf.createEntityManager());
getDao().getEm().getTransaction().begin();}/**
* After each test method, roll back the transaction started in the
*
* @Before method then close both the entity manager and entity manager
* factory.
*/
@After
publicvoid closeEmAndEmf(){
getDao().getEm().getTransaction().rollback();
getDao().getEm().close();
emf.close();}}
Now let’s see how this impacts the creation of our new BookTest class. We’ll start with everything but the tests and then look at each test.
Everything but the Tests
Here is the test class minus all of the tests.
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertNull;importjava.util.Calendar;importorg.junit.Test;importentity.Author;importentity.Book;importentity.Name;publicclass BookDaoTest extends BaseDbDaoTest {private BookDao dao;/**
* By overriding this method, I'm able to provide a dao to the base class,
* which then installs a new entity manager per test method execution. Note
* that my return type is not the same as the base class' version. I return
* BookDao whereas the base class returns BaseDao. Normally an overridden
* method must return the same type. However, it is OK for an overridden
* method to return a different type so long as that different type is a
* subclass of the type returned in the base class. This is called
* covariance.
*
* @see session.BaseDbDaoTest#getDao()
*/
@Overridepublic BookDao getDao(){if(dao == null){
dao = new BookDao();}return dao;}}
Creating a Book
We wrote this pretty much the same as in the Patron test. It might seem like we could get further reuse between tests and we could but at the cost of probably a bit too much indirection.
@Test
publicvoid createABook(){finalBook b = createABookImpl();finalBook found = getDao().retrieve(b.getId());
assertNotNull(found);}privateBook createABookImpl(){final Author a1 = new Author(newName("Bill", "Burke"));final Author a2 = new Author(newName("Richard", "Monson-Haefel"));return getDao().create("Enterprise JavaBeans 3.0", "978-0-596-00978-6",
Calendar.getInstance().getTime(), a1, a2);}
Removing a Book
This test method looks just like one in the PatronTest class. If you’re looking for an advanced exercise, consider moving all of the tests in the base class and making the derived class methods use them somehow. Warning, you might want to look up annotation inheritance.
@Test
publicvoid removeABook(){finalBook b = createABookImpl();Book found = getDao().retrieve(b.getId());
assertNotNull(found);
getDao().remove(b.getId());
found = getDao().retrieve(b.getId());
assertNull(found);}
Updating a Book
@Test
publicvoid updateABook(){finalBook b = createABookImpl();finalint initialAuthorCount = b.getAuthors().size();
b.addAuthor(new Author(newName("New", "Author")));
getDao().update(b);finalBook found = getDao().retrieve(b.getId());
assertEquals(initialAuthorCount + 1, found.getAuthors().size());}
Try to find a non- existant book
@Test
publicvoid tryToFindBookThatDoesNotExist(){finalBook b = getDao().retrieve(-1123123123l);
assertNull(b);}
Note that with the introduction of the base class we’ll also need to make changes to PatronTest. Here’s the updated version of PatronTest taking the new base class into consideration. PatronDaoTest.java Updated
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertFalse;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertNull;importorg.junit.Test;importentity.Address;importentity.Patron;/**
* This class has been updated to take advantage of BaseDbDaoTest. In reality, I
* just pulled the common functionality of pre test initialization and post test
* initialization to a base class since I'm going to use it across several test
* cases.
*/publicclass PatronDaoTest extends BaseDbDaoTest {privatestaticfinalString NEW_PN = "555-555-5555";private PatronDao dao;/**
* @see session.BaseDbDaoTest#getDao()
* @see session.BookDaoTest#getDao()
*/
@Overridepublic PatronDao getDao(){if(dao == null){
dao = new PatronDao();}return dao;}
@Test
publicvoid createAPatron(){final Patron p = createAPatronImpl();final Patron found = getDao().retrieve(p.getId());
assertNotNull(found);}/**
* I need to create patrons in several tests so it is factored out here.
*
* @return Newly created patron already inserted into the database under the
* current transaction
*/private Patron createAPatronImpl(){final Address a = new Address("5080 Spectrum Drive", "Suite 700 West",
"Addison", "TX", "75001");return getDao().createPatron("Brett", "Schuchert", "972-555-1212", a);}
@Test
publicvoid removeAPatron(){final Patron p = createAPatronImpl();
getDao().removePatron(p.getId());final Patron found = getDao().retrieve(p.getId());
assertNull(found);}
@Test
publicvoid updateAPatron(){final Patron p = createAPatronImpl();finalString originalPhoneNumber = p.getPhoneNumber();
p.setPhoneNumber(NEW_PN);
getDao().update(p);final Patron found = getDao().retrieve(p.getId());
assertNotNull(found);
assertFalse(NEW_PN.equals(originalPhoneNumber));
assertEquals(NEW_PN, p.getPhoneNumber());}
@Test
publicvoid tryToFindPatronThatDoesNotExist(){finalLong id = -18128129831298l;final Patron p = getDao().retrieve(id);
assertNull(p);}}
The Dao Classes
The BookDao looks a whole lot like the PatronDao: BookDao.java
packagesession;importjava.util.Date;importentity.Author;importentity.Book;/**
* This class offers the basic create, read, update, delete functions required
* for a book. As we implement more complex requirements, we'll be coming back
* to this class to add additional queries.
*/publicclass BookDao extends BaseDao {publicBook create(finalString title, finalString isbn,
finalDate publishDate, Author... authors){finalBook b = newBook(title, isbn, publishDate, authors);
getEm().persist(b);return b;}publicBook retrieve(finalLong id){return getEm().find(Book.class, id);}publicvoid remove(Long id){finalBook b = retrieve(id);if(b != null){
getEm().remove(b);}}publicvoid update(Book b){
getEm().merge(b);}}
Note that this class depends on a simple base class, the BaseDao, which offers support for storing the Entity Manager attribute: BaseDao.java
packagesession;importjavax.persistence.EntityManager;/**
* A simple base class for all dao's. It offers 2 features. First, it has the
* entity manager attribute. Second, it makes it possible to have a common test
* base class with the getDao() method to allow for automatic initialization.
*/publicabstractclass BaseDao {private EntityManager em;publicvoid setEm(final EntityManager em){this.em = em;}public EntityManager getEm(){return em;}}
And finally, here’s the updated PatronDao that has been rewritten to use the BaseDao. PatronDao.java
packagesession;importentity.Address;importentity.Patron;/**
* This class supports basic create, read, update, delete functionality for the
* Patron. As with Book, as we implement more requirements we'll be revisiting
* this class to extend its functionality.
*/publicclass PatronDao extends BaseDao {public Patron createPatron(finalString fname, finalString lname,
finalString phoneNumber, final Address a){final Patron p = new Patron(fname, lname, phoneNumber, a);
getEm().persist(p);return p;}public Patron retrieve(finalLong id){return getEm().find(Patron.class, id);}publicvoid removePatron(finalLong id){final Patron p = retrieve(id);if(p != null){
getEm().remove(p);}}public Patron update(final Patron p){return getEm().merge(p);}}
The Entity Model
We’ve added support for a Book and along the way we had to add in a few more classes. After the second test suite, we’re up to the following entities:
Entity
Description
Address
This entity represents the address for both an Author and a Patron. In the first tutorial we embedded this class. Now we’re allowing it to exist in its own table as a first-class citizen rather than embedding it.
Author
Books and Authors have a bi-directional, many to many relationship with each other. That is, a book has one to many Authors and an Author has one to many books. This entity represents one author and maintains a Set<Book> representing each of its books. We treat the Author as the secondary part of the relationship and the book as Primary.
Book
The book is a key entity in our system. It maintains a set of Authors and is considered the master of the bi-directional relationship. In version 1 of our system, the relationship between Books and Patrons is direct. We’ll change that in version 2.
Name
Authors and Patrons both have a name. Rather than duplicate the definition of names in both classes, we create a Name entity. This entity is embeddable, meaning its attributes will be stored as columns in the entities in which it is contained rather than as rows in a table all of its own.
Patron
The patron borrows books, so it has a Set<Books> as well as an embedded Name.
Now let’s review the code for each of these entities. As with previous examples, pay attention to the embedded comments.
Address
packageentity;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;/**
* This class will be known to JPA as an entity. It represents an address. It is
* a fairly simple class that gets stored in its own table called ADDRESS. The
* column names equal the names of the attributes.
*/
@Entitypublicclass Address {/**
* The next attribute is a key column in the database with a
* database-specific generated unique value.
*/
@Id
@GeneratedValue
privateLong id;/**
* The next attribute will be stored in a column with space for 50
* characters and cannot be null (the default)
*/
@Column(length = 50)privateString streetAddress1;/**
* The next attribute will be stored in a column with space for 50
* characters and it can be null(nullable = true).
*/
@Column(length = 50, nullable = true)privateString streetAddress2;
@Column(length = 20)privateString city;
@Column(length = 2)privateString state;
@Column(length = 9)privateString zip;public Address(){}public Address(finalString sa1, finalString sa2, finalString city,
finalString state, finalString zip){
setStreetAddress1(sa1);
setStreetAddress2(sa2);
setCity(city);
setState(state);
setZip(zip);}publicString getCity(){return city;}publicvoid setCity(finalString city){this.city = city;}publicString getState(){return state;}publicvoid setState(finalString state){this.state = state;}publicString getStreetAddress1(){return streetAddress1;}publicvoid setStreetAddress1(finalString streetAddress1){this.streetAddress1 = streetAddress1;}publicString getStreetAddress2(){return streetAddress2;}publicvoid setStreetAddress2(finalString streetAddress2){this.streetAddress2 = streetAddress2;}publicString getZip(){return zip;}publicvoid setZip(finalString zip){this.zip = zip;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
Author
packageentity;importjava.util.Set;importjavax.persistence.Embedded;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.ManyToMany;/**
* I am an entity with a bidirectional relationship to Book. The Book is
* considered the master of the relationship.
*/
@Entitypublicclass Author {
@Id
@GeneratedValue
privateLong id;/**
* The next attribute is embedded directly in me. That means its attributes
* will be directly stored in columns in the same table as me rather than
* being in its own table with key to itself and foreign key back to me.
*/
@Embedded
privateName name;/**
* A book might be written by several authors and an author might write
* several books. Therefore we maintain a many-to-many relationship between
* books authors. It's bidirectional as well.
*/
@ManyToMany
privateSet<Book> booksWritten;public Author(finalName name){
setName(name);}public Author(){}publicSet<Book> getBooksWritten(){return booksWritten;}publicvoid addBook(finalBook b){
booksWritten.add(b);
b.addAuthor(this);}publicvoid setBooksWritten(finalSet<Book> booksWritten){this.booksWritten = booksWritten;}publicLong getId(){return id;}publicvoid setId(finalLong id){this.id = id;}/**
* We are storing Authors in sets so we need to define some definition of
* equality. We've decided to use Name as that definition. You might think
* to use the id field for equality but it may not be assigned before this
* object is placed in a collection so we have to use a more natural
* definition of equality.
*/
@Overridepublicboolean equals(finalObject object){if(object instanceof Author){final Author rhs = (Author) object;return getName().equals(rhs.getName());}returnfalse;}/**
* The hash code should relate to the equals method. And as mentioned there,
* we cannot use the id field for the hash code because it is likely we
* won't have an id already assigned by the database before we try put this
* object in a collection that requires the hashCode method (such as HashSet
* or HashMap). So we use a natural part of the object for its
* interpretation of hash code.
*/
@Overridepublicint hashCode(){return getName().hashCode();}publicName getName(){return name;}publicvoid setName(finalName name){this.name = name;}}
Book
packageentity;importjava.util.Calendar;importjava.util.Date;importjava.util.HashSet;importjava.util.Set;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.ManyToMany;importjavax.persistence.ManyToOne;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I also have a many to many relationship with author. Since I define the
* mappedBy, I'm the (arbitrarily picked) master of the relationship. I also
* take care of cascading changes to the database.
*/
@EntitypublicclassBook{
@Id
@GeneratedValue
privateLong id;
@Column(length = 100, nullable = false)privateString title;
@Column(length = 20, nullable = false)privateString isbn;privateDate printDate;/**
* Authors may have written several books and vice-versa. We had to pick one
* side of this relationship as the primary one and we picked books. It was
* arbitrary but since we're dealing with books, we decided to make this
* side the primary size. The mappedBy connects this relationship to the one
* that is in Author. When we merge or persist, changes to this collection
* and the contents of the collection will be updated. That is, if we update
* the name of the author in the set, when we persist the book, the author
* will also get updated.
*
* Note that if we did not have the cascade setting here, they if we tried
* to persist a book with an unmanaged author (e.g. a newly created one),
* the entity manager would contain of a transient object.
*/
@ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
CascadeType.MERGE})privateSet<Author> authors;/**
* I may be borrowed. If so, then I'll know who that is. In this version, I
* simply have a direct relationship with the Patron. In the next version,
* we'll create a table to capture the details of borrowing a resource.
*/
@ManyToOne
private Patron borrowedBy;publicBook(finalString t, finalString i, finalDate printDate,
final Author... authors){
setTitle(t);
setIsbn(i);
setPrintDate(printDate);for(Author a : authors){
addAuthor(a);}}publicBook(){}publicSet<Author> getAuthors(){if(authors == null){
authors = newHashSet<Author>();}return authors;}publicvoid setAuthors(finalSet<Author> authors){this.authors = authors;}publicLong getId(){return id;}publicvoid setId(finalLong id){this.id = id;}publicString getIsbn(){return isbn;}publicvoid setIsbn(finalString isbn){this.isbn = isbn;}publicDate getPrintDate(){return printDate;}publicvoid setPrintDate(finalDate printDate){this.printDate = printDate;}publicString getTitle(){return title;}publicvoid setTitle(finalString title){this.title = title;}publicvoid addAuthor(final Author author){
getAuthors().add(author);}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceofBook&&((Book) rhs).getIsbn().equals(getIsbn());}
@Overridepublicint hashCode(){return getIsbn().hashCode();}publicboolean wasWrittenBy(Author a){return getAuthors().contains(a);}publicboolean checkedOutBy(Patron p){return p != null&& p.equals(getBorrowedBy());}publicDate calculateDueDateFrom(Date checkoutDate){finalCalendar c = Calendar.getInstance();
c.setTime(checkoutDate);
c.add(Calendar.DATE, 14);return c.getTime();}public Patron getBorrowedBy(){return borrowedBy;}publicvoid setBorrowedBy(Patron borrowedBy){this.borrowedBy = borrowedBy;}}
Name
packageentity;importjavax.persistence.Column;importjavax.persistence.Embeddable;/**
* Rather than repeat first name/last name in both Patron and Author, we create
* an embedded class. The fields of this class end up as columns in the table
* that contains the class that embeds this entity. That is, both author and
* patron will have a firstName and lastName column.
*/
@Embeddable
publicclassName{
@Column(length = 20, nullable = false)privateString firstName;
@Column(length = 30, nullable = false)privateString lastName;publicName(){}publicName(finalString firstName, finalString lastName){
setFirstName(firstName);
setLastName(lastName);}publicString getFirstName(){return firstName;}publicvoid setFirstName(String firstName){if(firstName != null){this.firstName = firstName;}else{this.firstName = "";}}publicString getLastName(){return lastName;}publicvoid setLastName(String lastName){if(lastName != null){this.lastName = lastName;}else{this.lastName = "";}}}
Patron
packageentity;importjava.util.HashSet;importjava.util.Set;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Embedded;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToMany;importjavax.persistence.OneToOne;
@Entitypublicclass Patron {
@Id
@GeneratedValue
privateLong id;
@Embedded
privateName name;
@Column(length = 11, nullable = false)privateString phoneNumber;/**
* This next field refers to an object that is stored in another table. All
* updates are cascaded. So if you persist me, my address, which is in
* another table, will be persisted automatically. Updates and removes are
* also cascaded automatically.
*
* Note that cascading removes is a bit dangerous. In this case I know that
* the address is owned by only one Patron. In general you need to be
* careful automatically removing objects in related tables due to possible
* constraint violations.
*/
@OneToOne(cascade = CascadeType.ALL)private Address address;/**
* A Patron may checkout several books. This collection
*/
@OneToMany(mappedBy = "borrowedBy", cascade = { CascadeType.MERGE,
CascadeType.PERSIST})privateSet<Book> borrowedBooks;public Patron(finalString fName, finalString lName, finalString phone,
final Address a){
setName(newName(fName, lName));
setPhoneNumber(phone);
setAddress(a);}public Address getAddress(){return address;}publicvoid setAddress(Address address){this.address = address;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}publicString getPhoneNumber(){return phoneNumber;}publicvoid setPhoneNumber(String phoneNumber){this.phoneNumber = phoneNumber;}publicSet<Book> getBorrowedBooks(){if(borrowedBooks == null){
borrowedBooks = newHashSet<Book>();}return borrowedBooks;}publicvoid setBorrowedBooks(Set<Book> borrowedBooks){this.borrowedBooks = borrowedBooks;}publicvoid addBook(finalBook b){
getBorrowedBooks().add(b);}publicvoid removeBook(finalBook b){
getBorrowedBooks().remove(b);}publicName getName(){return name;}publicvoid setName(Name name){this.name = name;}}
V1 Third Test Suite
Typical enterprise systems are build on a multi-tiered system. There are usually at least three tiers:
Presentation
Business
Integration
There might be a few more, but for now this list of tiers will suite us well.
Our first two tests produced Data Access Objects (dao)'s. These two dao's hide the details of getting books and patrons. They fall under the integration layer.
Now it is time to add a higher-level concept, the Library. The Library class represents a Facade to the underlying system. This so-called facade will be the primary interface to the middle tier of our system.
Of course, along the way we'll end up doing yet more refactoring to accommodate this new suite of tests.
Library
First we'll start with a new suite of tests for this Library facade. For this first pass, we'll write several basic tests and a few tests that move us closer to use-case like functionality.
Adding a Book
@Test
publicvoid addBook(){finalBook b = createBook();Set<Author> authors = b.getAuthors();finalBook found = library.findBookById(b.getId());
assertTrue(found.getAuthors().containsAll(authors));}privateBook createBook(){final Author a1 = new Author(newName("Christian", "Bauer"));final Author a2 = new Author(newName("Gavin", "King"));return library.createBook("Hibernate In Action", ISBN, Calendar
.getInstance().getTime(), a1, a2);}
Lookup a Book that Does Not Exist
Notice that this test has different results than the same test in the BookDaoTest. In this case we expect an exception to be thrown while in the case of the BookDaoTest we just get back null. Why? The dao has no way of knowing what the policy should be regarding not finding objects, whereas the Library facade can set the policy.
@Test
publicvoid addPatron(){final Patron p = createPatron();final Patron found = library.findPatronById(p.getId());
assertNotNull(found);}private Patron createPatron(){final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
"TX", "75001");return library.createPatron(PATRON_ID, "Brett", "Schuchert",
"555-1212", a);}
Lookup a Patron that Does Not Exist
As with the BookDao, the PatronDao simply returns null if an object is not found by ID. The Library changes that null result into an exception.
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookThatDoesNotExist(){final Patron p = createPatron();
library.checkout(p.getId(), ID_DNE);}
Checkout a Book to a Patron that Does Not Exist
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookToPatronThatDoesNotExist(){finalBook b = createBook();
library.checkout(ID_DNE, b.getId());}
LibraryTest.java
Here's the shell of the test.
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertFalse;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertTrue;importjava.util.Calendar;importjava.util.Set;importjavax.persistence.EntityNotFoundException;importorg.junit.Before;importorg.junit.Test;importentity.Address;importentity.Author;importentity.Book;importentity.Name;importentity.Patron;importexception.BookAlreadyCheckedOut;publicclass LibraryTest extends EntityManagerBasedTest {privatestaticfinallong ID_DNE = -443123222l;privatestaticfinalString PATRON_ID = "113322";privatestaticfinalString ISBN = "1-932394-15-X";private Library library;
@Before
publicvoid setupLibrary(){final BookDao bd = new BookDao();
bd.setEm(getEm());final PatronDao pd = new PatronDao();
pd.setEm(getEm());
library = new Library();
library.setBookDao(bd);
library.setPatronDao(pd);}}
EntityManagerBasedTest
This new class inherits from a new base class called EnttyManagerBasedTest. This class factors out just the part of initialization related to the entity manager and the transactions from the BaseDbDaoTest.
packagesession;importjavax.persistence.EntityManager;importjavax.persistence.EntityManagerFactory;importjavax.persistence.Persistence;importorg.apache.log4j.BasicConfigurator;importorg.apache.log4j.Level;importorg.apache.log4j.Logger;importorg.junit.After;importorg.junit.Before;importorg.junit.BeforeClass;/**
* Our tests use an entity manager. The first pass at the BaseDbDaoTest forced
* initialization of a Dao. That works for the dao-based tests but not all
* tests. This class factors out just the part that sets up and cleans up the
* entity manager.
*
*/publicabstractclass EntityManagerBasedTest {private EntityManagerFactory emf;private EntityManager em;/**
* Once before the tests start running for a given class, init the logger
* with a basic configuration and set the default reporting layer to error
* for all classes whose package starts with org.
*/
@BeforeClass
publicstaticvoid initLogger(){// Produce minimal output.
BasicConfigurator.configure();// Comment this line to see a lot of initialization// status logging.Logger.getLogger("org").setLevel(Level.ERROR);}/**
* Before each test method, look up the entity manager factory, then create
* the entity manager.
*/
@Before
publicvoid initEmfAndEm(){
emf = Persistence.createEntityManagerFactory("lis");
em = emf.createEntityManager();
em.getTransaction().begin();}/**
* After each test method, roll back the transaction started in the -at-
* Before method then close both the entity manager and entity manager
* factory.
*/
@After
publicvoid closeEmAndEmf(){
getEm().getTransaction().rollback();
getEm().close();
emf.close();}public EntityManager getEm(){return em;}publicvoid setEm(EntityManager em){this.em = em;}}
BaseDbDaoTest
Here is yet another updated BaseDbDaoTest that reflects the new base class.
packagesession;importorg.junit.Before;/**
* A base class for tests that handles logger initialization, entity manager
* factory and entity manager creation, associating an entity manager with a
* dao, starting and rolling back transactions.
*/publicabstractclass BaseDbDaoTest extends EntityManagerBasedTest {/**
* Derived class is responsible for instantiating the dao. This method gives
* the hook necessary to this base class to init the dao with an entity
* manger in a per-test setup method.
*
* @return The dao to be used for a given test. The type specified is a base
* class from which all dao's inherit. The test derived class will
* override this method and change the return type to the type of
* dao it uses. This is called **covariance**. Java 5 allows
* covariant return types. I.e. BookDaoTest's version of getDao()
* will return BookDao while PatronDao's version of getDao() will
* return Patron.
*/publicabstract BaseDao getDao();/**
* The -at- before method in the base class executes first. After that, init
* the dao with the entity manager.
*/
@Before
publicvoid initDao(){
getDao().setEm(getEm());}}
The Exception
We've added one new unchecked exception to our system, BookAlreadyCheckedOut. Here it is:
packageexception;/**
* A simple unchecked exception reflecting a particular business rule violation.
* A book cannot be checked out if it is already checked out.
*
* This exception inherits from RuntimeException (or it is an unchecked
* exception). Why? The policy of whether to use checked or unchecked exceptions
* is project dependent. We are using this for learning about EJB3 and JPA and
* NOT about how to write exceptions, so using one policy versus the other is
* arbitrary for our purposes. Working with unchecked exceptions is a bit looser
* but also keeps the code looking a bit cleaner, so we've gone with unchecked
* exceptions.
*/publicclass BookAlreadyCheckedOut extendsRuntimeException{privatestaticfinallong serialVersionUID = 2286908621531520488L;finalLong bookId;public BookAlreadyCheckedOut(finalLong bookId){this.bookId = bookId;}publicLong getBookId(){return bookId;}}
Library
This class is all new.
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.EntityNotFoundException;importentity.Address;importentity.Author;importentity.Book;importentity.Patron;importexception.BookAlreadyCheckedOut;publicclass Library {private BookDao bookDao;private PatronDao patronDao;public BookDao getBookDao(){return bookDao;}publicvoid setBookDao(BookDao bookDao){this.bookDao = bookDao;}public PatronDao getPatronDao(){return patronDao;}publicvoid setPatronDao(PatronDao patronDao){this.patronDao = patronDao;}publicBook createBook(finalString title, finalString isbn,
finalDate date, final Author a1, final Author a2){return getBookDao().create(title, isbn, date, a1, a2);}publicList<Book> findBookByIsbn(String isbn){return getBookDao().findByIsbn(isbn);}public Patron createPatron(finalString patronId, finalString fname,
finalString lname, finalString phoneNumber, final Address a){return getPatronDao().createPatron(fname, lname, phoneNumber, a);}public Patron findPatronById(finalLong id){final Patron p = getPatronDao().retrieve(id);if(p == null){thrownew EntityNotFoundException(String.format("Patron with id: %d does not exist", id));}return p;}publicvoid checkout(finalLong patronId, finalLong bookId){finalBook b = findBookById(bookId);if(b.isOnLoan()){thrownew BookAlreadyCheckedOut(bookId);}final Patron p = findPatronById(patronId);
p.addBook(b);
b.setBorrowedBy(p);
getPatronDao().update(p);}publicBook findBookById(Long id){finalBook b = getBookDao().findById(id);if(b == null){thrownew EntityNotFoundException(String.format("Book with Id:%d does not exist", id));}return b;}publicvoid returnBook(Long id){finalBook b = getBookDao().findById(id);if(b.isOnLoan()){final Patron p = b.checkin();
p.removeBook(b);
getPatronDao().update(p);}}}
BookDao
The tests use the findByIsbn() method, which returns a collection of Books. Why does findByIsbn() return a collection of books? The isbn is not unique; the book id is the only unique column. If we enforced a unique isbn, then there could only be one book of a given isbn in the library.
We've also added a method, findById, which should return a unique value (or null).
We need a basic utility to assist with equality. This utility will handle when we have null references.
EqualsUtil
packageutil;/**
* We typically need to compare two object and also perform null checking. This
* class provides a simple wrapper to accomplish doing so.
*/publicclass EqualsUtil {private EqualsUtil(){// I'm a utility class, do not instantiate me}publicstaticboolean equals(finalObject lhs, finalObject rhs){return lhs == null&& rhs == null
|| (lhs != null&& rhs != null&& lhs.equals(rhs));}}
Book
The book is somewhat changed. First it needs to import util.EqualsUtil (as shown below). It also contains some named queries and three new methods: isOnLoanTo, isOnLoan and checkin. The code below shows these changes.
importutil.EqualsUtil;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I also have a many to many relationship with author. Since I define the
* mappedBy, I'm the (arbitrarily picked) master of the relationship. I also
* take care of cascading changes to the database.
*/
@Entity/**
* A named query must have a globally unique name. That is why these are named
* "Book."... These queries could be associated with any entity. Given that they
* clearly deal with books, it seems appropriate to put them here. These will
* probably be pre-compiled and in any case available from the entity manager by
* using em.getNamedQuery("Book.findById").
*/
@NamedQueries({
@NamedQuery(name = "Book.findById",
query = "SELECT b FROM Book b where b.id = :id"),
@NamedQuery(name = "Book.findByIsbn",
query = "SELECT b FROM Book b WHERE b.isbn = :isbn")})publicclassBook{publicboolean isOnLoanTo(final Patron foundPatron){return EqualsUtil.equals(getBorrowedBy(), foundPatron);}publicboolean isOnLoan(){return getBorrowedBy()!= null;}public Patron checkin(){final Patron p = getBorrowedBy();
setBorrowedBy(null);return p;}}
Patron
There was only one change to Patron. We want to be able to ask the Patron if it is in fact borrowing a particular book.
JPA Tutorial 3 - A Mini Application
In this example we start with a simple domain model and incrementally migrate it to become more realistic. Along the way we end up using several features of JPA not yet covered by the previous tutorials.The Problem
V1 Project Setup
For the rest of this section, when you see <project>, replace it with JpaTutorial3Create Java Project
Next we need to create a Java project. We'll keep the source separate from the bin directory:Create folders and packages
Add Required Libraries
We now need to add two libraries. Note that these steps assume you've already worked through the first tutorial and are working in the same workspace. If you, you'll need to create user libraries. Review Creating User Libraries.If you'd like some background information on JUnit, please go here.
Configure Persistence Unit
We now need to create the Persistent Unit definition. We are going to create a file called persistence.xml in the src/META-INF directory with the following contents:persistence.xml
The Steps
Update persistence.xml
Update Persistence Unit NameThe name of the persistence unit in your just-copied persistence.xml is examplePersistenceUnit in this example we use lis for Library Information System. Make the following change:
Your project is now set up.
V1 First Test Suite
The Unit Tests
A Little ContextBefore we get started, this tutorial is deliberately organized in an inconvenient fashion. Why? My target reader is someone in a class I'm teaching (the material is open-source but still targeted). I do not want the student to be able to quickly just copy the whole thing and get it to work without having to put forth some effort. In a classroom situation, I have all of the source code available if I need to help a student get up to speed.
We'll start with a stripped down version of the requirements. This first test suite handles the following test cases:
Notice that this suite of tests is for Creating, Reading, Updating and Deleting (CRUD) Patrons.
Assuming you've done Tutorial 2, much of the boilerplate code is going to look the same. First let's write a unit test for each of these test cases:
Create a Patron
@Test public void createAPatron() { final Patron p = createAPatronImpl(); final Patron found = dao.retrieve(p.getId()); assertNotNull(found); } private Patron createAPatronImpl() { final Address a = new Address("5080 Spectrum Drive", "Suite 700 West", "Addison", "TX", "75001"); return dao.createPatron("Brett", "Schuchert", "972-555-1212", a); }This test first creates a patron using a private utility method. This method exists because it is used later in other unit tests.
Looking at the test, it uses an attribute called dao. This is a Data Access Object (which we'll later convert to a stateless Session Bean). This Data Access Object will be responsible for retrieving, creating and removing Patrons.
Remove a Patron
@Test public void removeAPatron() { final Patron p = createAPatronImpl(); dao.removePatron(p.getId()); final Patron found = dao.retrieve(p.getId()); assertNull(found); }This test uses the utility method to create a patron. It then removes it and makes sure that when we try to retrieve it that the Patron no longer exists.
Update a Patron
@Test public void updateAPatron() { final Patron p = createAPatronImpl(); final String originalPhoneNumber = p.getPhoneNumber(); p.setPhoneNumber(NEW_PN); dao.update(p); final Patron found = dao.retrieve(p.getId()); assertNotNull(found); assertFalse(NEW_PN.equals(originalPhoneNumber)); assertEquals(NEW_PN, p.getPhoneNumber()); }We create a patron then update it.Attempt to find Patron that does not exist
@Test public void tryToFindPatronThatDoesNotExist() { final Long id = -18128129831298l; final Patron p = dao.retrieve(id); assertNull(p); }Verify that when we try to find a patron that's not found, we get back null.
Supporting Code
We have several veterans returning from previous tutorials. And here they are:PatronDaoTest
First the imports and the attributes. Note that this is a complete class that will compile. It just doesn't do anything yet.
Initialization of the Logger
This is our 1-time initialization of the logging system.
@BeforeClass public static void initLogger() { // Produce minimal output. BasicConfigurator.configure(); // Comment this line to see a lot of initialization // status logging. Logger.getLogger("org").setLevel(Level.ERROR); }Getting EMF and EM
Now before each unit test we'll look up the entity manager factory, create a dao, create an entity manager and pass it into a DAO and finally start a transaction.
@Before public void initEmfAndEm() { emf = Persistence.createEntityManagerFactory("lis"); dao = new PatronDao(); dao.setEm(emf.createEntityManager()); dao.getEm().getTransaction().begin(); }Clean up after each test
After each test we'll rollback the transaction we created in the pre-test initialization. We'll then close both the entity manager and entity manager factory. This keeps our tests isolated.
@After public void closeEmAndEmf() { dao.getEm().getTransaction().rollback(); dao.getEm().close(); emf.close(); }The Entities
We need to create entities. These entities are a bit more well-specified that what you've seen in the previous tutorials. In most cases I believe the extra information is intuitive. Where it is not, I'll try to point out what is going on.Address.java
Patron.java
Finally, the Data Access Object
The DAO has the following four methods:
We'll look at each of these, although none of this will be new if you've looked at the first tutorial.
createPatron
Given the information to create a patron, instantiate one and then persiste it. Note that this is written in a style that will natually fit into a Session Bean.
retrieve
This uses the find method built into the entity manager. It returns null if not found. It first sees if the object is in the first-level cache. If not, it retrieves it from the database.
removePatron
To remove an object we have to find it first. You do not provide a class and a key. So we first retrieve it (it might already be in the cache so this may not involve a database hit. We then issue the remove of the object.
update
Update uses the merge method to get its work done. Note that it returns what is returned from merge. Why? The provided patron could be detached (retrieve during another transaction or from a different instance of an entity manager. If this is the case, then the object will not be put into the cache (and therefore become managed). Instead, a new instance is created and the contents of the paramter is copied into the new, managed instance. That new managed instance is returned. If the provided patron is managed, then there's actually not need to even call this method because any changes made to the patron will be reflected in the patron after the transaction is closed.
The rest of the class
Here are the imports, attributes and getters/setters.
V1 Second Test Suite
We started with Patron. In round 2, we add the basic support for the Book. The book dao needs the same basic tests:Note that we did not also include retrieving a book. We use this functionality in all of the tests anyway so I do not include a specific test for that functionality. This might seem like we’re not isolating tests perfectly but then I’ve never seen or come up with a “perfect” solution to this issue and this seems adequate to me.
We've already written a test very much like the above list if you consider PatronTest. We can extract quite a bit of common code out of our PatronTest and reuse it in our BookTest class. Take a look at this base class (note the embedded comments contain background information):
BaseDbDaoTest.java
Now let’s see how this impacts the creation of our new BookTest class. We’ll start with everything but the tests and then look at each test.
Everything but the Tests
Here is the test class minus all of the tests.
Creating a Book
We wrote this pretty much the same as in the Patron test. It might seem like we could get further reuse between tests and we could but at the cost of probably a bit too much indirection.
@Test public void createABook() { final Book b = createABookImpl(); final Book found = getDao().retrieve(b.getId()); assertNotNull(found); } private Book createABookImpl() { final Author a1 = new Author(new Name("Bill", "Burke")); final Author a2 = new Author(new Name("Richard", "Monson-Haefel")); return getDao().create("Enterprise JavaBeans 3.0", "978-0-596-00978-6", Calendar.getInstance().getTime(), a1, a2); }Removing a Book
This test method looks just like one in the PatronTest class. If you’re looking for an advanced exercise, consider moving all of the tests in the base class and making the derived class methods use them somehow. Warning, you might want to look up annotation inheritance.
@Test public void removeABook() { final Book b = createABookImpl(); Book found = getDao().retrieve(b.getId()); assertNotNull(found); getDao().remove(b.getId()); found = getDao().retrieve(b.getId()); assertNull(found); }Updating a Book
@Test public void updateABook() { final Book b = createABookImpl(); final int initialAuthorCount = b.getAuthors().size(); b.addAuthor(new Author(new Name("New", "Author"))); getDao().update(b); final Book found = getDao().retrieve(b.getId()); assertEquals(initialAuthorCount + 1, found.getAuthors().size()); }Try to find a non- existant book
@Test public void tryToFindBookThatDoesNotExist() { final Book b = getDao().retrieve(-1123123123l); assertNull(b); }Note that with the introduction of the base class we’ll also need to make changes to PatronTest. Here’s the updated version of PatronTest taking the new base class into consideration.
PatronDaoTest.java Updated
The Dao Classes
The BookDao looks a whole lot like the PatronDao:BookDao.java
Note that this class depends on a simple base class, the BaseDao, which offers support for storing the Entity Manager attribute:
BaseDao.java
And finally, here’s the updated PatronDao that has been rewritten to use the BaseDao.
PatronDao.java
The Entity Model
We’ve added support for a Book and along the way we had to add in a few more classes. After the second test suite, we’re up to the following entities:Now let’s review the code for each of these entities. As with previous examples, pay attention to the embedded comments.
Address
Author
Book
Name
Patron
V1 Third Test Suite
Typical enterprise systems are build on a multi-tiered system. There are usually at least three tiers:There might be a few more, but for now this list of tiers will suite us well.
Our first two tests produced Data Access Objects (dao)'s. These two dao's hide the details of getting books and patrons. They fall under the integration layer.
Now it is time to add a higher-level concept, the Library. The Library class represents a Facade to the underlying system. This so-called facade will be the primary interface to the middle tier of our system.
Of course, along the way we'll end up doing yet more refactoring to accommodate this new suite of tests.
Library
First we'll start with a new suite of tests for this Library facade. For this first pass, we'll write several basic tests and a few tests that move us closer to use-case like functionality.Adding a Book
@Test public void addBook() { final Book b = createBook(); Set<Author> authors = b.getAuthors(); final Book found = library.findBookById(b.getId()); assertTrue(found.getAuthors().containsAll(authors)); } private Book createBook() { final Author a1 = new Author(new Name("Christian", "Bauer")); final Author a2 = new Author(new Name("Gavin", "King")); return library.createBook("Hibernate In Action", ISBN, Calendar .getInstance().getTime(), a1, a2); }Lookup a Book that Does Not Exist
Notice that this test has different results than the same test in the BookDaoTest. In this case we expect an exception to be thrown while in the case of the BookDaoTest we just get back null. Why? The dao has no way of knowing what the policy should be regarding not finding objects, whereas the Library facade can set the policy.
Adding a Patron
@Test public void addPatron() { final Patron p = createPatron(); final Patron found = library.findPatronById(p.getId()); assertNotNull(found); } private Patron createPatron() { final Address a = new Address("5080 Spectrum Drive", "", "Dallas", "TX", "75001"); return library.createPatron(PATRON_ID, "Brett", "Schuchert", "555-1212", a); }Lookup a Patron that Does Not Exist
As with the BookDao, the PatronDao simply returns null if an object is not found by ID. The Library changes that null result into an exception.
Checking out a book to a patron
@Test public void checkoutBook() { final Book b = createBook(); final Patron p = createPatron(); library.checkout(p.getId(), b.getId()); final Book foundBook = library.findBookById(b.getId()); final Patron foundPatron = library.findPatronById(p.getId()); assertTrue(foundBook.isOnLoanTo(foundPatron)); assertTrue(foundPatron.isBorrowing(foundBook)); }Returning a book
@Test public void returnBook() { final Book b = createBook(); final Patron p = createPatron(); library.checkout(p.getId(), b.getId()); final int booksBefore = p.getBorrowedBooks().size(); assertTrue(b.isOnLoan()); library.returnBook(b.getId()); assertEquals(booksBefore - 1, p.getBorrowedBooks().size()); assertFalse(b.isOnLoan()); }Returning a book that is not checked out
@Test public void returnBookThatsNotCheckedOut() { final Book b = createBook(); assertFalse(b.isOnLoan()); library.returnBook(b.getId()); assertFalse(b.isOnLoan()); }Checking out a Book that is Already Checked Out
Checkout a Book that Does Not Exist
Checkout a Book to a Patron that Does Not Exist
LibraryTest.java
Here's the shell of the test.
EntityManagerBasedTest
This new class inherits from a new base class called EnttyManagerBasedTest. This class factors out just the part of initialization related to the entity manager and the transactions from the BaseDbDaoTest.
BaseDbDaoTest
Here is yet another updated BaseDbDaoTest that reflects the new base class.
The Exception
We've added one new unchecked exception to our system, BookAlreadyCheckedOut. Here it is:Library
This class is all new.
BookDao
The tests use the findByIsbn() method, which returns a collection of Books. Why does findByIsbn() return a collection of books? The isbn is not unique; the book id is the only unique column. If we enforced a unique isbn, then there could only be one book of a given isbn in the library.
We've also added a method, findById, which should return a unique value (or null).
Util
We need a basic utility to assist with equality. This utility will handle when we have null references.EqualsUtil
EqualsUtilTest
Entity Changes
Book
The book is somewhat changed. First it needs to import util.EqualsUtil (as shown below). It also contains some named queries and three new methods: isOnLoanTo, isOnLoan and checkin. The code below shows these changes.
Patron
There was only one change to Patron. We want to be able to ask the Patron if it is in fact borrowing a particular book.
V1 Assignments
JPA Tutorial 3 - V1 AssignmentsV2 Requirements: Relational Table
JPA Tutorial 3 - V2 RequirementsV2 Updated Library Test Suite
JPA Tutorial 3 - V2 First Test SuiteV2 Updated Sessions
JPA Tutorial 3 - V2 Updated Sessionsv2 Updated Entities
JPA Tutorial 3 - V2 Updated EntitiesV2 Assignments
JPA Tutorial 3 - V2 AssignmentsJPA Tutorial 3 - FAQ
<-Back